package com.lb.uts.util;

import com.lb.uts.exception.RscException;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpMessage;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.ParseException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.config.RequestConfig.Builder;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager;
import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;
import org.apache.http.impl.nio.reactor.IOReactorConfig;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.nio.reactor.ConnectingIOReactor;
import org.apache.http.nio.reactor.IOReactorException;
import org.apache.http.protocol.HTTP;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.UnsupportedCharsetException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Future;

/**
 * 远程服务调用工具--HTTP异步方式
 *
 * @author liangb
 *
 */
public class RscHttpAsyncUtils {

    private static CloseableHttpAsyncClient httpclient = configHttpClient(5000, 5000, 100, 100, 100);

    private static final String defaultCharset = "UTF-8";

    private static SSLContext createSSLContext() {
        // Trust standard CAs and all self-signed certs
        SSLContext sslcontext = null;
        try {
            sslcontext = SSLContexts.custom()
                    .loadTrustMaterial(new TrustSelfSignedStrategy())
                    .build();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        }

        return sslcontext;
    }

    /**
     * 设置HTTP的相关参数，超时时间在每个请求时可以另行设置
     *
     * @param connectTimeout 默认连接超时,连接建立时间,三次握手完成时间
     * @param socketTimeout 默认请求超时,数据传输过程中数据包之间间隔的最大时间
     * @param connectionRequestTimeout 默认使用连接池来管理连接,从连接池获取连接的超时时间
     * @param maxTotal 连接池中最大连接数
     * @param maxPerRoute 分配给同一个route(路由)最大的并发连接数,route为运行环境机器到目标机器的一条线路,举例来说,我们使用HttpClient的实现来分别请求 www.baidu.com 的资源和 www.bing.com 的资源那么他就会产生两个route
     */
    public static void setHttpclient(int connectTimeout, int socketTimeout, int connectionRequestTimeout, int maxTotal, int maxPerRoute) {
        httpclient = configHttpClient(connectTimeout, socketTimeout, connectionRequestTimeout, maxTotal, maxPerRoute);
    }

    /**
     * 设置HTTP的相关参数，超时时间再每个请求时可以另行设定
     *
     * @param connectTimeout 默认连接超时,连接建立时间,三次握手完成时间
     * @param socketTimeout 默认请求超时,数据传输过程中数据包之间间隔的最大时间
     * @param connectionRequestTimeout 默认使用连接池来管理连接,从连接池获取连接的超时时间
     * @param maxTotal 连接池中最大连接数
     * @param maxPerRoute 分配给同一个route(路由)最大的并发连接数,route为运行环境机器到目标机器的一条线路,举例来说,我们使用HttpClient的实现来分别请求 www.baidu.com 的资源和 www.bing.com 的资源那么他就会产生两个route
     */
    private static CloseableHttpAsyncClient configHttpClient(int connectTimeout, int socketTimeout, int connectionRequestTimeout, int maxTotal, int maxPerRoute) {
        // IO 超时时间
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(connectTimeout)	// 连接超时,连接建立时间,三次握手完成时间
                .setSocketTimeout(socketTimeout)	// 请求超时,数据传输过程中数据包之间间隔的最大时间
                .setConnectionRequestTimeout(connectionRequestTimeout)	// 使用连接池来管理连接,从连接池获取连接的超时时间
                .build();

        // 配置IO线程
        IOReactorConfig ioReactorConfig = IOReactorConfig.custom().
                setIoThreadCount(Runtime.getRuntime().availableProcessors())	// IO线程数设置为CPU内核数一致
                .setSoKeepAlive(true)
                .build();

        ConnectingIOReactor ioReactor=null;
        try {
            ioReactor = new DefaultConnectingIOReactor(ioReactorConfig);
        } catch (IOReactorException e) {
            e.printStackTrace();
        }

        // 配置连接池大小
        PoolingNHttpClientConnectionManager connManager = new PoolingNHttpClientConnectionManager(ioReactor);
        connManager.setMaxTotal(maxTotal);			// 连接池中最大连接数
        connManager.setDefaultMaxPerRoute(maxPerRoute);	// 分配给同一个route(路由)最大的并发连接数,route为运行环境机器到目标机器的一条线路,举例来说,我们使用HttpClient的实现来分别请求 www.baidu.com 的资源和 www.bing.com 的资源那么他就会产生两个route

        CloseableHttpAsyncClient httpclient = HttpAsyncClients.custom().
                setConnectionManager(connManager)
                .setDefaultRequestConfig(requestConfig)
                .setSSLContext(createSSLContext())
                .build();

        return httpclient;
    }

    /**
     * POST方式发起远程调用
     * @param url 调用地址
     * @param headers 请求头
     * @param params 请求参数
     * @param timeOutMillis 超时时间（毫秒）
     * @param charset 字符编码
     * @param callback 回调处理类
     * @return 返回报文
     * @throws RscException
     */
    public static Future<HttpResponse> post(String url, Map<String, String> headers, Map<String, String> params, Integer timeOutMillis, String charset, FutureCallback<HttpResponse> callback) {
        httpclient.start();

        HttpPost post = new HttpPost(url);

        if (StringUtils.isEmpty(charset)) {
            charset = defaultCharset;
        }

        List<NameValuePair> nvps = new ArrayList<NameValuePair>();
        if (params != null) {
            for (Entry<String, String> entry : params.entrySet()) {
                nvps.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
            }
        }

        //添加请求头
        addHeader(post, headers);

        //设置超时时间
        setConfig(post, timeOutMillis);

        try {
            post.setEntity(new UrlEncodedFormEntity(nvps, charset));
        } catch (UnsupportedEncodingException e) {
            throw new RscException(e);
        }

        return httpclient.execute(post, callback);
    }

    /**
     * POST方式发起远程调用
     * @param url 调用地址
     * @param headers 请求头
     * @param body 直接的请求内容（非键值对），例如：JSON串
     * @param timeOutMillis 超时时间（毫秒）
     * @param charset 字符编码
     * @param callback 回调处理类
     * @return 返回报文
     * @throws RscException
     */
    public static Future<HttpResponse> post(String url, Map<String, String> headers, String body, Integer timeOutMillis, String charset, FutureCallback<HttpResponse> callback) {
        httpclient.start();

        HttpPost post = new HttpPost(url);

        if (StringUtils.isEmpty(charset)) {
            charset = defaultCharset;
        }

        //添加请求头
        addHeader(post, headers);

        //设置超时时间
        setConfig(post, timeOutMillis);

        try {
            StringEntity stringEntity = new StringEntity(body, charset);
            stringEntity.setContentEncoding(new BasicHeader(HTTP.CONTENT_ENCODING, "UTF-8"));
            post.setEntity(stringEntity);
        } catch (UnsupportedCharsetException e) {
            throw new RscException(e);
        }

        return httpclient.execute(post, callback);
    }

    /**
     * POST方式发起远程调用，默认为UTF-8编码
     * @param url 调用地址
     * @param params 请求参数
     * @param timeOutMillis 超时时间（毫秒）
     * @param callback 回调处理类
     * @return 返回报文
     * @throws RscException
     */
    public static Future<HttpResponse> post(String url, Map<String, String> params, Integer timeOutMillis, FutureCallback<HttpResponse> callback) {
        return post(url, null, params, timeOutMillis, defaultCharset, callback);
    }

    /**
     * POST方式发起远程调用
     * @param url 调用地址
     * @param params 请求参数
     * @param timeOutMillis 超时时间（毫秒）
     * @param charset
     * @param callback 回调处理类
     * @return 返回报文
     * @throws RscException
     */
    public static Future<HttpResponse> post(String url, Map<String, String> params, Integer timeOutMillis, String charset, FutureCallback<HttpResponse> callback) {
        return post(url, null, params, timeOutMillis, charset, callback);
    }

    /**
     * POST方式发起远程调用，默认为UTF-8编码
     * @param url 调用地址
     * @param params 请求参数
     * @param callback 回调处理类
     * @return 返回报文
     * @throws RscException
     */
    public static Future<HttpResponse> post(String url, Map<String, String> params, FutureCallback<HttpResponse> callback) {
        return post(url, null, params, null, defaultCharset, callback);
    }

    /**
     * POST方式发起远程调用
     * @param url 调用地址
     * @param params 请求参数
     * @param charset 字符编码
     * @param callback 回调处理类
     * @return 返回报文
     * @throws RscException
     */
    public static Future<HttpResponse> post(String url, Map<String, String> params, String charset, FutureCallback<HttpResponse> callback) {
        return post(url, null, params, null, charset, callback);
    }

    /**
     * GET方式发起远程调用
     * @param url 调用地址，参数直接在url中带入
     * @param headers 请求头
     * @param timeOutMillis 超时时间（毫秒）
     * @param charset 字符编码
     * @param callback 回调处理类
     * @return 返回报文
     * @throws RscException
     */
    public static Future<HttpResponse> get(String url, Map<String, String> headers, Integer timeOutMillis, String charset, FutureCallback<HttpResponse> callback) {
        httpclient.start();

        if (StringUtils.isEmpty(charset)) {
            charset = defaultCharset;
        }

        HttpGet httpGet = new HttpGet(url);

        //添加请求头
        addHeader(httpGet, headers);

        //设置超时时间
        setConfig(httpGet, timeOutMillis);

        //发起异步请求
        return httpclient.execute(httpGet, callback);
    }

    /**
     * GET方式发起远程调用
     * @param url 调用地址，不带参数
     * @param headers 请求头
     * @param params 请求参数
     * @param timeOutMillis 超时时间（毫秒）
     * @param charset 字符编码
     * @param callback 回调处理类
     * @return 返回报文
     * @throws RscException
     */
    public static Future<HttpResponse> get(String url, Map<String, String> headers, Map<String, String> params, Integer timeOutMillis, String charset, FutureCallback<HttpResponse> callback) {
        httpclient.start();

        if (StringUtils.isEmpty(charset)) {
            charset = defaultCharset;
        }
        if (params != null && !params.isEmpty()) {
            List<NameValuePair> pairs = new ArrayList<NameValuePair>(params.size());
            for (Entry<String, String> entry : params.entrySet()) {
                String value = entry.getValue();
                if (StringUtils.isNotEmpty(value)) {
                    pairs.add(new BasicNameValuePair(entry.getKey(), value));
                }
            }
            try {
                url += "?" + EntityUtils.toString(new UrlEncodedFormEntity(pairs, charset), charset);
            } catch (ParseException e) {
                throw new RscException(e);
            } catch (UnsupportedEncodingException e) {
                throw new RscException(e);
            } catch (IOException e) {
                throw new RscException(e);
            }
        }

        return get(url, headers, timeOutMillis, charset, callback);
    }

    /**
     * GET方式发起远程调用
     * @param url 调用地址，不带参数
     * @param params 请求参数
     * @param charset 字符编码
     * @param callback 回调处理类
     * @return 返回报文
     * @throws RscException
     */
    public static Future<HttpResponse> get(String url, Map<String, String> params, String charset, FutureCallback<HttpResponse> callback) {
        return get(url, null, params, null, charset, callback);
    }

    /**
     * GET方式发起远程调用，默认为UTF-8编码
     * @param url 调用地址，不带参数
     * @param params 请求参数
     * @param callback 回调处理类
     * @return 返回报文
     * @throws RscException
     */
    public static Future<HttpResponse> get(String url, Map<String, String> params, FutureCallback<HttpResponse> callback) {
        return get(url, null, params, null, defaultCharset, callback);
    }

    /**
     * GET方式发起远程调用，默认为UTF-8编码
     * @param url 调用地址，参数直接在url中带入
     * @param headers 请求头
     * @param timeOutMillis 超时时间（毫秒）
     * @param callback 回调处理类
     * @return 返回报文
     * @throws RscException
     */
    public static Future<HttpResponse> get(String url, Map<String, String> headers, Integer timeOutMillis, FutureCallback<HttpResponse> callback) {
        return get(url, null, timeOutMillis, defaultCharset, callback);
    }

    /**
     * GET方式发起远程调用，默认为UTF-8编码
     * @param url 调用地址，参数直接在url中带入
     * @param timeOutMillis 超时时间（毫秒）
     * @param callback 回调处理类
     * @return 返回报文
     * @throws RscException
     */
    public static Future<HttpResponse> get(String url, Integer timeOutMillis, FutureCallback<HttpResponse> callback) {
        return get(url, null, timeOutMillis, defaultCharset, callback);
    }

    /**
     * GET方式发起远程调用
     * @param url 调用地址，参数直接在url中带入
     * @param timeOutMillis 超时时间（毫秒）
     * @param charset 字符编码
     * @param callback 回调处理类
     * @return 返回报文
     * @throws RscException
     */
    public static Future<HttpResponse> get(String url, Integer timeOutMillis, String charset, FutureCallback<HttpResponse> callback) {
        return get(url, null, timeOutMillis, charset, callback);
    }

    /**
     * 配置连接属性
     * @param request
     */
    private static void setConfig(HttpRequestBase request, Integer timeOutMillis) {
        Builder builder = RequestConfig.custom();

        //超时时间
        if (timeOutMillis != null) {
            builder.setConnectTimeout(timeOutMillis);
            builder.setSocketTimeout(timeOutMillis);
            builder.setConnectionRequestTimeout(timeOutMillis);
        }

        request.setConfig(builder.build());
    }

    /**
     * 添加请求头
     * @param request
     * @param headers
     */
    private static void addHeader(HttpMessage request, Map<String, String> headers) {
        if (headers != null) {
            for (Entry<String, String> entry : headers.entrySet()) {
                request.addHeader(entry.getKey(), entry.getValue());
            }
        }
    }

}